using Crux.Core.Websockets.Metadata;
using Nito.AsyncEx;

namespace Crux.Core.Websockets;
/// <summary>
/// LocalLifetimeManagerOfT
/// </summary>
public class LocalLifetimeManager<THandler>
    : WebSocketLifetimeManager<THandler>,
      IDisposableObservable
    where THandler : WebSocketHandler
{
    /// <summary>
    ///
    /// </summary>
    private readonly WebSocketConnectionStore _connections;
    /// <summary>
    ///
    /// </summary>
    private readonly GroupList _groups;
    /// <summary>
    ///
    /// </summary>
    private readonly ILogger _logger;
    /// <summary>
    ///
    /// </summary>
    /// <returns></returns>
    private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    /// <summary>
    ///
    /// </summary>
    /// <value></value>
    public bool IsDisposed { get; private set; }
    /// <summary>
    ///
    /// </summary>
    /// <param name="logger"></param>
    public LocalLifetimeManager(
        ILogger<LocalLifetimeManager<THandler>> logger)
    {
        this._logger = logger
            ?? throw new ArgumentNullException(nameof(logger));
        this._groups = new GroupList(cancellationTokenSource.Token);
        this._connections = new WebSocketConnectionStore(cancellationTokenSource.Token);
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="connectionId"></param>
    /// <param name="groupName"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public override Task AddToGroupAsync(
        string connectionId,
        string groupName,
        CancellationToken cancellationToken = default)
    {
        Requires.NotNullOrEmpty(connectionId, nameof(connectionId));
        Requires.NotNullOrEmpty(groupName, nameof(groupName));

        var connection = _connections[connectionId];
        if (connection is null) return Task.CompletedTask;

        _groups.Add(connection, groupName);
        return Task.CompletedTask;
    }
    /// <summary>
    ///
    /// </summary>
    public void Dispose()
    {
        if (this.IsDisposed) return;

        this.IsDisposed = true;
        this.cancellationTokenSource.Cancel();
        GC.SuppressFinalize(this);
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="webSocketConnectionContext"></param>
    /// <returns></returns>
    public override Task OnConnectedAsync(WebSocketConnectionContext webSocketConnectionContext)
    {
        _connections.Add(webSocketConnectionContext);
        return Task.CompletedTask;
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="webSocketConnectionContext"></param>
    /// <returns></returns>
    public override Task OnDisconnectedAsync(WebSocketConnectionContext webSocketConnectionContext)
    {
        _connections.Remove(webSocketConnectionContext);
        _groups.RemoveDisconnectedConnection(webSocketConnectionContext.ConnectionId);
        return Task.CompletedTask;
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="connectionId"></param>
    /// <param name="groupName"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public override Task RemoveToGroupAsync(
        string connectionId,
        string groupName,
        CancellationToken cancellationToken = default)
    {
        Requires.NotNullOrEmpty(connectionId, nameof(connectionId));
        Requires.NotNullOrEmpty(groupName, nameof(groupName));

        var connection = _connections[connectionId];
        if (connection is null) return Task.CompletedTask;

        _groups.Remove(connectionId, groupName);
        return Task.CompletedTask;
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="methodName"></param>
    /// <param name="args"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public override Task SendAllAsync(
        string methodName,
        object[] args,
        CancellationToken cancellationToken = default)
         => SendToAllConnections(
             methodName,
             args,
             cancellationToken: cancellationToken);
    /// <summary>
    ///
    /// </summary>
    /// <param name="methodName"></param>
    /// <param name="args"></param>
    /// <param name="include"></param>
    /// <param name="state"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    private Task SendToAllConnections(
        string methodName,
        object[] args,
        Func<WebSocketConnectionContext, object?, bool>? include = null,
        object? state = null,
        CancellationToken cancellationToken = default)
    {
        List<Task>? tasks = null;
        var message = new NotifyMessage
        {
            Type = MessageType.Notify,
            Target = methodName,
            Arguments = args
        };

        foreach (var connection in _connections)
        {
            if (include is not null && !include(connection, state))
            {
                continue;
            }

            var task = connection.SendAsync(message);
            if (!task.IsCompleted)
            {
                if (tasks is null)
                {
                    tasks = new List<Task>();
                }
                tasks.Add(task);
            }
            else
            {
                AsyncContext.Run(async () => await task.ConfigureAwait(false));
            }
        }
        if (tasks is null) return Task.CompletedTask;

        return Task.WhenAll(tasks);
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="methodName"></param>
    /// <param name="args"></param>
    /// <param name="exceptList"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public override Task SendAllExceptAsync(
        string methodName,
        object[] args,
        IReadOnlyList<string> exceptList,
        CancellationToken cancellationToken = default)
        => SendToAllConnections(
            methodName,
            args,
            include: (connection, state) => !((IReadOnlyList<string>)state!).Contains(connection.ConnectionId),
            state: exceptList,
            cancellationToken: cancellationToken);
    /// <summary>
    ///
    /// </summary>
    /// <param name="connectionId"></param>
    /// <param name="methodName"></param>
    /// <param name="args"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public override Task SendConnectionAsync(string connectionId, string methodName, object[] args, CancellationToken cancellationToken = default)
    {
        Requires.NotNullOrEmpty(connectionId, nameof(connectionId));

        var connection = _connections[connectionId];
        if (connection is null) return Task.CompletedTask;

        return connection.SendAsync(new NotifyMessage
        {
            Type = MessageType.Notify,
            Target = methodName,
            Arguments = args
        });
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="connectionIds"></param>
    /// <param name="methodName"></param>
    /// <param name="args"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public override Task SendConnectionsAsync(
        IReadOnlyList<string> connectionIds,
        string methodName,
        object[] args,
        CancellationToken cancellationToken = default)
        => SendToAllConnections(
            methodName,
            args,
            include: (connection, state) => ((IReadOnlyList<string>)state!).Contains(connection.ConnectionId),
            state: connectionIds,
            cancellationToken: cancellationToken);
    /// <summary>
    ///
    /// </summary>
    /// <param name="groupName"></param>
    /// <param name="methodName"></param>
    /// <param name="args"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public override Task SendGroupAsync(
        string groupName,
        string methodName,
        object[] args,
        CancellationToken cancellationToken = default)
    {
        Requires.NotNullOrEmpty(groupName, nameof(groupName));

        var group = _groups[groupName];
        if (group is null) return Task.CompletedTask;

        List<Task>? tasks = null;
        NotifyMessage? message = null;

        SendToGroupConnections(
            methodName,
            args,
            group,
            include: null,
            state: null,
            tasks: ref tasks,
            message: ref message,
            cancellationToken: cancellationToken);
        if (tasks is null) return Task.CompletedTask;

        return Task.WhenAll(tasks);
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="methodName"></param>
    /// <param name="args"></param>
    /// <param name="connections"></param>
    /// <param name="include"></param>
    /// <param name="state"></param>
    /// <param name="tasks"></param>
    /// <param name="message"></param>
    /// <param name="cancellationToken"></param>
    private void SendToGroupConnections(
        string methodName,
        object[] args,
        ConcurrentDictionary<string, WebSocketConnectionContext> connections,
        Func<WebSocketConnectionContext, object?, bool>? include,
        object? state,
        ref List<Task>? tasks,
        ref NotifyMessage? message,
        CancellationToken cancellationToken)
    {
        foreach (var connection in connections)
        {
            if (include is not null && !include(connection.Value, state))
            {
                continue;
            }

            if (message is null)
            {
                message = new NotifyMessage
                {
                    Type = MessageType.Notify,
                    Target = methodName,
                    Arguments = args
                };
            }

            var task = connection.Value.SendAsync(message);
            if (!task.IsCompleted)
            {
                if (tasks is null)
                {
                    tasks = new List<Task>();
                }
                tasks.Add(task);
            }
            else
            {
                AsyncContext.Run(async () => await task.ConfigureAwait(false));
            }
        }
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="groupName"></param>
    /// <param name="methodName"></param>
    /// <param name="args"></param>
    /// <param name="connectionIds"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public override Task SendGroupExceptAsync(
        string groupName,
        string methodName,
        object[] args,
        IReadOnlyList<string> connectionIds,
        CancellationToken cancellationToken = default)
    {
        Requires.NotNullOrEmpty(groupName, nameof(groupName));

        var group = _groups[groupName];
        if (group is null) return Task.CompletedTask;

        List<Task>? tasks = null;
        NotifyMessage? message = null;

        SendToGroupConnections(
            methodName,
            args,
            group,
            include: (connection, state) => !((IReadOnlyList<string>)state!).Contains(connection.ConnectionId),
            state: connectionIds,
            tasks: ref tasks,
            message: ref message,
            cancellationToken: cancellationToken);
        if (tasks is null) return Task.CompletedTask;

        return Task.WhenAll(tasks);
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="groupNames"></param>
    /// <param name="methodName"></param>
    /// <param name="args"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public override Task SendGroupsAsync(
        IReadOnlyList<string> groupNames,
        string methodName,
        object[] args,
        CancellationToken cancellationToken = default)
    {
        List<Task>? tasks = null;
        NotifyMessage? message = null;

        foreach (var groupName in groupNames)
        {
            if (string.IsNullOrEmpty(groupName))
                throw new InvalidOperationException("Cannot send to an empty group name.");

            var group = _groups[groupName];
            if (group is null) continue;

            SendToGroupConnections(
                methodName,
                args,
                group,
                include: null,
                state: null,
                tasks: ref tasks,
                message: ref message,
                cancellationToken: cancellationToken);
        }

        if (tasks is null) return Task.CompletedTask;

        return Task.WhenAll(tasks);
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="userId"></param>
    /// <param name="methodName"></param>
    /// <param name="args"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public override Task SendUserAsync(
        string userId,
        string methodName,
        object[] args,
        CancellationToken cancellationToken = default)
        => SendToAllConnections(
            methodName,
            args,
            include: (connection, state) => string.Equals(connection.UserIdentifier, (string)state!, StringComparison.Ordinal),
            state: userId,
            cancellationToken: cancellationToken);
    /// <summary>
    ///
    /// </summary>
    /// <param name="userId"></param>
    /// <param name="methodName"></param>
    /// <param name="args"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public override Task SendUsersAsync(
        IReadOnlyList<string> userId,
        string methodName,
        object[] args,
        CancellationToken cancellationToken = default)
        => SendToAllConnections(
            methodName,
            args,
            include: (connection, state) => ((IReadOnlyList<string>)state!).Contains(connection.UserIdentifier),
            state: userId,
            cancellationToken: cancellationToken);
}
